package com.celesea.configuration.web;

import com.celesea.framework.api.HttpKit;
import com.celesea.framework.api.Message;
import com.celesea.framework.api.Tip;
import com.celesea.framework.exception.*;
import com.celesea.framework.holder.BusinessCodeHolder;
import com.celesea.framework.holder.PropertiesHolder;
import com.celesea.framework.util.StringKit;
import com.netflix.hystrix.exception.HystrixRuntimeException;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.omg.CORBA.SystemException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.method.HandlerMethod;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * @author JackYuan
 * @create 2017-11-01 10:15
 */

@org.springframework.web.bind.annotation.ControllerAdvice
public class ExceptionAdvice {

    private Logger logger = LoggerFactory.getLogger(getClass());
    private static final String REMOTE_TRACE_KEY = "trace";

    @ExceptionHandler({Throwable.class})
    @ResponseBody
    public Object handlerException(
            HttpServletRequest request,
            HttpServletResponse response,
            HandlerMethod handlerMethod,
            Throwable throwable) throws Throwable {
        // fetch root Exception wraped by HystrixRuntimeException
        logger.error("server exception：", throwable);

        String businessCode = BusinessCodeHolder.get();

        Throwable cause = throwable;
        for (Throwable tmp = throwable; tmp != null; tmp = tmp.getCause()) {
            if (tmp instanceof BaseException || tmp instanceof RemoteException
                    || tmp instanceof SocketTimeoutException) {
                cause = tmp;
                break;
            }
        }

        Tip<Object> result = HttpKit.failed(null);
        RemoteException.ErrorInfo errorInfo = defaultErrorInfo(request, cause);
        if (cause instanceof AccessDeniedException) {
            result = HttpKit.unauthorized();
        } else if (cause instanceof BaseException) {
            BaseException e = BaseException.class.cast(cause);
            result.setCode(Integer.parseInt(e.code()));

            if (cause instanceof BusinessException) {
                result.setMessage(
                        PropertiesHolder.getProperty(Message.BUSINESS_ERROR,
                                wrapBusinessCode(businessCode), e.getMessage()));
            } else if (cause instanceof ParameterException) {
                result.setMessage(
                        PropertiesHolder.getProperty(Message.PARAMETER_ERROR,
                                wrapBusinessCode(businessCode), e.getMessage()));
            } else if (cause instanceof RepeatedException) {
                result.setMessage(
                        PropertiesHolder.getProperty(Message.REPEATED_ERROR,
                                wrapBusinessCode(businessCode)));
            } else if (cause instanceof SystemException) {
                result.setMessage(
                        PropertiesHolder.getProperty(Message.SYSTEM_ERROR,
                                wrapBusinessCode(businessCode), e.getMessage()));
            } else if (cause instanceof AuthorizationException) {
                result.setMessage(
                        PropertiesHolder.getProperty(Message.UNAUTHORIZED, e.getMessage()));
            } else if (cause instanceof AuthenticationException) {
                result.setMessage(
                        PropertiesHolder.getProperty(Message.FORBIDDEN, e.getMessage()));
            } else if (cause instanceof VersionException) {
                result.setMessage(
                        PropertiesHolder.getProperty(Message.VERSION_ERROR,
                                wrapBusinessCode(businessCode)));
            }
        } else if (cause instanceof HystrixRuntimeException) {
            response.setStatus(Message.SERVICE_UNAVAILABLE);
            result.setCode(Integer.parseInt(StringKit.defaultIfBlank(businessCode, String.valueOf(Message.SERVICE_UNAVAILABLE))));
            result.setMessage(PropertiesHolder.getProperty(Message.SERVICE_UNAVAILABLE, "不可用"));
        } else if (cause instanceof SocketTimeoutException) {
            response.setStatus(Message.SERVICE_UNAVAILABLE);
            result.setCode(Integer.parseInt(StringKit.defaultIfBlank(businessCode, String.valueOf(Message.SERVICE_UNAVAILABLE))));
            result.setMessage(PropertiesHolder.getProperty(Message.SERVICE_UNAVAILABLE, "超时"));
            errorInfo = defaultErrorInfo(request, throwable);
        } else if (cause instanceof MethodArgumentNotValidException) {
            MethodArgumentNotValidException e = (MethodArgumentNotValidException) cause;
            List<String> list = new ArrayList<>();
            for (FieldError fieldError : e.getBindingResult().getFieldErrors()) {
                String error = String.format("%1$s%2$s", fieldError.getField(),
                        fieldError.getDefaultMessage());
                list.add(error);
            }
//            response.setStatus(Message.PARAMETER_ERROR);
            result.setCode(Integer.parseInt(StringKit.defaultIfBlank(businessCode, String.valueOf(Message.PARAMETER_ERROR))));
            result.setMessage(StringUtils.join(list, ","));
            errorInfo = defaultErrorInfo(request, throwable);
        } else if (cause instanceof RuntimeException) {
            result.setMessage(cause.getMessage());
        } else {
            response.setStatus(Message.INTERNAL_SERVER_ERROR);
            result.setCode(Message.INTERNAL_SERVER_ERROR);
            result.setMessage(PropertiesHolder.getProperty(Message.INTERNAL_SERVER_ERROR));
            errorInfo = defaultErrorInfo(request, throwable);
        }

        if (errorInfo != null && StringKit.isEmpty(errorInfo.getStatus())) {
            errorInfo.setStatus(String.valueOf(response.getStatus()));
        }
        result.setContent(errorInfo);
        return result;
    }


    private String wrapBusinessCode(String businessCode) {
        return StringKit.isEmpty(businessCode) ? "" : "【" + businessCode + "】";
    }

    private RemoteException.ErrorInfo defaultErrorInfo(HttpServletRequest request, Throwable throwable) {
        if (this.isIncludeStackTrace(request, MediaType.ALL)) {
            RemoteException.ErrorInfo errorInfo = new RemoteException.ErrorInfo();

            errorInfo.setTimestamp(new Date());
            errorInfo.setException(throwable.getClass().getName());
            errorInfo.setPath(request.getServletPath());
            errorInfo.setRequestChain(request.getServletPath());
            errorInfo.setTrace(ExceptionUtils.getStackTrace(throwable));
            return errorInfo;
        }
        return null;
    }

    public boolean isIncludeStackTrace(HttpServletRequest request, MediaType produces) {
//        ErrorProperties.IncludeStacktrace include = serverProperties.getError().getIncludeStacktrace();
        ErrorProperties.IncludeStacktrace include = ErrorProperties.IncludeStacktrace.ON_TRACE_PARAM;
        if (include == ErrorProperties.IncludeStacktrace.ALWAYS) {
            return true;
        }
        if (include == ErrorProperties.IncludeStacktrace.ON_TRACE_PARAM) {
            return getTraceParameter(request) || getTraceHeader(request);
        }
        return false;
    }

    private boolean getTraceParameter(HttpServletRequest request) {
        String parameter = request.getParameter("trace");
        if (parameter == null) {
            return false;
        }
        return !"false".equals(parameter.toLowerCase());
    }

    private boolean getTraceHeader(HttpServletRequest request) {
        String trace = request.getHeader(REMOTE_TRACE_KEY);
        if (trace == null) {
            return false;
        }
        return !"false".equals(trace.toLowerCase());
    }
}
